@ampless/runtime 0.2.0-alpha.9 → 1.0.0-alpha.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ja.md CHANGED
@@ -34,10 +34,10 @@ export const ampless = createAmpless({
34
34
  })
35
35
  ```
36
36
 
37
- `app/site/[siteId]/` 以下のルートとディスパッチャーはワンライナーのファクトリー呼び出しになります:
37
+ `app/` 直下のルートとディスパッチャーはワンライナーのファクトリー呼び出しになります:
38
38
 
39
39
  ```ts
40
- // app/site/[siteId]/page.tsx
40
+ // app/page.tsx
41
41
  import { ampless } from '@/lib/ampless'
42
42
  import {
43
43
  createThemeHomeDispatcher,
@@ -50,7 +50,7 @@ export default createThemeHomeDispatcher(ampless)
50
50
  ```
51
51
 
52
52
  ```ts
53
- // app/site/[siteId]/og/[slug]/route.ts
53
+ // app/og/[slug]/route.ts
54
54
  import { ampless } from '@/lib/ampless'
55
55
  import { createOgRouteHandler } from '@ampless/runtime/routes'
56
56
 
@@ -59,22 +59,48 @@ export const dynamic = 'force-dynamic'
59
59
  export const GET = createOgRouteHandler(ampless)
60
60
  ```
61
61
 
62
- ミドルウェア:
62
+ ミドルウェア(proxy):
63
63
 
64
64
  ```ts
65
- // middleware.ts
65
+ // proxy.ts (Next.js 16 で middleware.ts から改名)
66
66
  import cmsConfig from './cms.config'
67
- import { createAmplessMiddleware, defaultMatcherConfig } from '@ampless/runtime/middleware'
67
+ import outputs from './amplify_outputs.json'
68
+ import { createAmplessMiddleware } from '@ampless/runtime/middleware'
68
69
 
69
- export const middleware = createAmplessMiddleware({ cmsConfig })
70
- export const config = defaultMatcherConfig
70
+ export const proxy = createAmplessMiddleware({
71
+ cmsConfig,
72
+ appsyncUrl: outputs.data.url,
73
+ apiKey: outputs.data.api_key!,
74
+ })
75
+
76
+ // matcher はインライン定義 — Next.js 16 の Turbopack は `export const config`
77
+ // が静的オブジェクトリテラルであることを要求します。
78
+ export const config = {
79
+ matcher: [
80
+ '/((?!admin|api|login|_next/static|_next/image|favicon\\.ico|amplify_outputs\\.json).*)',
81
+ ],
82
+ }
71
83
  ```
72
84
 
85
+ middleware はリクエストごとに AppSync から `post.format` /
86
+ `post.metadata` / `post.updatedAt` を取得(apiKey 認証)し、slug をキー
87
+ にした 200 エントリの LRU(60 秒 TTL、Lambda モジュールスコープ)で
88
+ キャッシュした上で、適切な内部ハンドラーにリクエストを書き換えます。
89
+
90
+ - 通常の投稿 → 書き換えなし、`app/[slug]/page.tsx` で配信
91
+ - `metadata.no_layout: true` HTML / `format: 'static'` → `/r/<slug>(/<path>)`、
92
+ `app/r/[slug]/[[...path]]/route.ts` で配信
93
+
94
+ また、`post.metadata.cache`(auto / deep / hot)+ `post.updatedAt` +
95
+ `cms.config.cache.{cooldownMs, freshTtlSeconds, deepTtlSeconds}` から
96
+ `Cache-Control` を算出してレスポンスに付与します。キャッシュ戦略の
97
+ 契約については `docs/CONTENT.md` を参照してください。
98
+
73
99
  ## サブパス
74
100
 
75
101
  - `@ampless/runtime` — `createAmpless`、ランタイム型、`renderBody`・`renderThemeCss`・フォーマットコンバーターの再エクスポート
76
102
  - `@ampless/runtime/middleware` — `createAmplessMiddleware`、`defaultMatcherConfig`
77
- - `@ampless/runtime/routes` — `createOgRouteHandler`、`createSitemapRouteHandler`、`createFeedRouteHandler`、`createRawRouteHandler`
103
+ - `@ampless/runtime/routes` — `createOgRouteHandler`、`createSitemapRouteHandler`、`createFeedRouteHandler`、`createUnderscoreRouteHandler`
78
104
  - `@ampless/runtime/dispatchers` — `createThemeHomeDispatcher`、`createThemePostDispatcher`、`createThemeTagDispatcher`(それぞれ対応する `*Metadata` ファクトリーあり)
79
105
 
80
106
  ## テンプレートに残るもの
package/README.md CHANGED
@@ -34,10 +34,10 @@ export const ampless = createAmpless({
34
34
  })
35
35
  ```
36
36
 
37
- Routes and dispatchers under `app/site/[siteId]/` become one-line factory invocations:
37
+ Routes and dispatchers under `app/` become one-line factory invocations:
38
38
 
39
39
  ```ts
40
- // app/site/[siteId]/page.tsx
40
+ // app/page.tsx
41
41
  import { ampless } from '@/lib/ampless'
42
42
  import {
43
43
  createThemeHomeDispatcher,
@@ -50,7 +50,7 @@ export default createThemeHomeDispatcher(ampless)
50
50
  ```
51
51
 
52
52
  ```ts
53
- // app/site/[siteId]/og/[slug]/route.ts
53
+ // app/og/[slug]/route.ts
54
54
  import { ampless } from '@/lib/ampless'
55
55
  import { createOgRouteHandler } from '@ampless/runtime/routes'
56
56
 
@@ -59,22 +59,48 @@ export const dynamic = 'force-dynamic'
59
59
  export const GET = createOgRouteHandler(ampless)
60
60
  ```
61
61
 
62
- Middleware:
62
+ Middleware (proxy):
63
63
 
64
64
  ```ts
65
- // middleware.ts
65
+ // proxy.ts (Next.js 16 rename of middleware.ts)
66
66
  import cmsConfig from './cms.config'
67
- import { createAmplessMiddleware, defaultMatcherConfig } from '@ampless/runtime/middleware'
67
+ import outputs from './amplify_outputs.json'
68
+ import { createAmplessMiddleware } from '@ampless/runtime/middleware'
68
69
 
69
- export const middleware = createAmplessMiddleware({ cmsConfig })
70
- export const config = defaultMatcherConfig
70
+ export const proxy = createAmplessMiddleware({
71
+ cmsConfig,
72
+ appsyncUrl: outputs.data.url,
73
+ apiKey: outputs.data.api_key!,
74
+ })
75
+
76
+ // Inline the matcher — Next.js 16's Turbopack requires
77
+ // `export const config` to be a static object literal.
78
+ export const config = {
79
+ matcher: [
80
+ '/((?!admin|api|login|_next/static|_next/image|favicon\\.ico|amplify_outputs\\.json).*)',
81
+ ],
82
+ }
71
83
  ```
72
84
 
85
+ The middleware fetches `post.format` / `post.metadata` / `post.updatedAt`
86
+ from AppSync (apiKey auth) on each request, caches the result in a
87
+ 200-entry LRU keyed by slug (60-second TTL, Lambda module scope), and
88
+ rewrites the request to the right internal handler:
89
+
90
+ - themed post → no rewrite, served by `app/[slug]/page.tsx`
91
+ - `metadata.no_layout: true` HTML / `format: 'static'` → `/r/<slug>(/<path>)`,
92
+ served by `app/r/[slug]/[[...path]]/route.ts`
93
+
94
+ It also computes `Cache-Control` from `post.metadata.cache` (auto /
95
+ deep / hot) + `post.updatedAt` + `cms.config.cache.{cooldownMs,
96
+ freshTtlSeconds, deepTtlSeconds}` and sets the header on the response.
97
+ See `docs/CONTENT.md` for the cache strategy contract.
98
+
73
99
  ## Sub-paths
74
100
 
75
101
  - `@ampless/runtime` — `createAmpless`, runtime types, and re-exports of `renderBody`, `renderThemeCss`, format converters
76
102
  - `@ampless/runtime/middleware` — `createAmplessMiddleware`, `defaultMatcherConfig`
77
- - `@ampless/runtime/routes` — `createOgRouteHandler`, `createSitemapRouteHandler`, `createFeedRouteHandler`, `createRawRouteHandler`
103
+ - `@ampless/runtime/routes` — `createOgRouteHandler`, `createSitemapRouteHandler`, `createFeedRouteHandler`, `createUnderscoreRouteHandler`
78
104
  - `@ampless/runtime/dispatchers` — `createThemeHomeDispatcher`, `createThemePostDispatcher`, `createThemeTagDispatcher` (each with a matching `*Metadata` factory)
79
105
 
80
106
  ## What's still in the template
@@ -4,16 +4,14 @@ import { Ampless } from '../index.js';
4
4
  import 'ampless';
5
5
 
6
6
  interface Props$2 {
7
- params: Promise<{
8
- siteId: string;
9
- }>;
7
+ params: Promise<Record<string, never>>;
10
8
  }
11
9
  type ThemeHomeDispatcher = (props: Props$2) => Promise<ReactNode>;
12
10
  type ThemeHomeMetadata = (props: Props$2) => Promise<Metadata>;
13
11
  /**
14
- * Home page dispatcher. Resolves the active theme for the request's
15
- * siteId and renders the theme's `components.Home` server component
16
- * with the same `params` Promise it was passed.
12
+ * Home page dispatcher. Resolves the active theme and renders the
13
+ * theme's `components.Home` server component with the same `params`
14
+ * Promise it was passed.
17
15
  */
18
16
  declare function createThemeHomeDispatcher(ampless: Ampless): ThemeHomeDispatcher;
19
17
  /** generateMetadata factory for the home dispatcher. */
@@ -21,29 +19,23 @@ declare function createThemeHomeMetadata(ampless: Ampless): ThemeHomeMetadata;
21
19
 
22
20
  interface Props$1 {
23
21
  params: Promise<{
24
- siteId: string;
25
22
  slug: string;
26
23
  }>;
27
24
  }
28
25
  type ThemePostDispatcher = (props: Props$1) => Promise<ReactNode>;
29
26
  type ThemePostMetadata = (props: Props$1) => Promise<Metadata>;
30
27
  /**
31
- * Post page dispatcher. Resolves the active theme and renders the
32
- * theme's `components.Post` server component.
33
- *
34
- * Before delegating, the dispatcher peeks at the post's metadata: if
35
- * `metadata.no_layout === true`, the post is meant to be served as
36
- * bare HTML (own DOCTYPE, no theme chrome, no Next.js root layout).
37
- * Next.js page.tsx can't bypass the root layout, so we 308-redirect
38
- * to the raw route handler at `/raw/<slug>` which returns the body
39
- * unchanged. The redirect is permanent because no_layout is a
40
- * persistent property of the post — bookmarks naturally settle on
41
- * the `/raw/` URL.
28
+ * Themed post page dispatcher. Resolves the active theme and renders
29
+ * the theme's `components.Post` server component.
42
30
  *
43
- * The metadata peek is an extra AppSync call before theme resolve,
44
- * but it's the same query the theme's Post component would make
45
- * anyway; AppSync's query-level dedupe within a single request keeps
46
- * the wire cost flat in practice.
31
+ * Routing decision (no_layout HTML vs static bundle vs themed) lives
32
+ * in middleware now middleware fetches `post.format` /
33
+ * `post.metadata` / `post.updatedAt` from AppSync once per slug
34
+ * (Lambda-memory LRU, 60s TTL) and rewrites the request to either
35
+ * this dispatcher (themed render) or the unified route handler at
36
+ * `/r/<slug>(/...)` (no_layout HTML or static bundle). That keeps the
37
+ * decision in one place and avoids the dispatcher's extra AppSync
38
+ * round-trip when the post is a themed render.
47
39
  *
48
40
  * If the theme doesn't declare a Post component at all, returns
49
41
  * Next.js's notFound() (404).
@@ -54,7 +46,6 @@ declare function createThemePostMetadata(ampless: Ampless): ThemePostMetadata;
54
46
 
55
47
  interface Props {
56
48
  params: Promise<{
57
- siteId: string;
58
49
  tag: string;
59
50
  }>;
60
51
  }
@@ -1,36 +1,24 @@
1
1
  // src/dispatchers/home.ts
2
2
  function createThemeHomeDispatcher(ampless) {
3
3
  return async function SiteHomeDispatcher({ params }) {
4
- const { siteId } = await params;
5
- const { module } = await ampless.resolveActiveTheme(siteId);
4
+ const { module } = await ampless.resolveActiveTheme();
6
5
  const Home = module.components.Home;
7
6
  return await Home({ params });
8
7
  };
9
8
  }
10
9
  function createThemeHomeMetadata(ampless) {
11
10
  return async function generateMetadata({ params }) {
12
- const { siteId } = await params;
13
- const { module } = await ampless.resolveActiveTheme(siteId);
11
+ const { module } = await ampless.resolveActiveTheme();
14
12
  const fn = module.metadata?.Home;
15
13
  return fn ? await fn({ params }) : {};
16
14
  };
17
15
  }
18
16
 
19
17
  // src/dispatchers/post.ts
20
- import { notFound, redirect } from "next/navigation";
18
+ import { notFound } from "next/navigation";
21
19
  function createThemePostDispatcher(ampless) {
22
20
  return async function SitePostDispatcher({ params }) {
23
- const { siteId, slug } = await params;
24
- const post = await ampless.getPublishedPost(slug, { siteId });
25
- if (post?.metadata?.no_layout === true) {
26
- redirect(`/raw/${slug}`);
27
- }
28
- if (post?.format === "static") {
29
- const body = post.body ?? null;
30
- const entrypoint = typeof body?.entrypoint === "string" && body.entrypoint ? body.entrypoint : "index.html";
31
- redirect(`/${slug}/${entrypoint}`);
32
- }
33
- const { module } = await ampless.resolveActiveTheme(siteId);
21
+ const { module } = await ampless.resolveActiveTheme();
34
22
  const Post = module.components.Post;
35
23
  if (!Post) notFound();
36
24
  return await Post({ params });
@@ -38,8 +26,7 @@ function createThemePostDispatcher(ampless) {
38
26
  }
39
27
  function createThemePostMetadata(ampless) {
40
28
  return async function generateMetadata({ params }) {
41
- const { siteId } = await params;
42
- const { module } = await ampless.resolveActiveTheme(siteId);
29
+ const { module } = await ampless.resolveActiveTheme();
43
30
  const fn = module.metadata?.Post;
44
31
  return fn ? await fn({ params }) : {};
45
32
  };
@@ -49,8 +36,7 @@ function createThemePostMetadata(ampless) {
49
36
  import { notFound as notFound2 } from "next/navigation";
50
37
  function createThemeTagDispatcher(ampless) {
51
38
  return async function SiteTagDispatcher({ params }) {
52
- const { siteId } = await params;
53
- const { module } = await ampless.resolveActiveTheme(siteId);
39
+ const { module } = await ampless.resolveActiveTheme();
54
40
  const Tag = module.components.Tag;
55
41
  if (!Tag) notFound2();
56
42
  return await Tag({ params });
@@ -58,8 +44,7 @@ function createThemeTagDispatcher(ampless) {
58
44
  }
59
45
  function createThemeTagMetadata(ampless) {
60
46
  return async function generateMetadata({ params }) {
61
- const { siteId } = await params;
62
- const { module } = await ampless.resolveActiveTheme(siteId);
47
+ const { module } = await ampless.resolveActiveTheme();
63
48
  const fn = module.metadata?.Tag;
64
49
  return fn ? await fn({ params }) : {};
65
50
  };
package/dist/index.d.ts CHANGED
@@ -25,7 +25,6 @@ interface StorageApi {
25
25
 
26
26
  interface PublicPostShape {
27
27
  postId: string;
28
- siteId: string;
29
28
  slug: string;
30
29
  title: string;
31
30
  excerpt?: string | null;
@@ -35,13 +34,18 @@ interface PublicPostShape {
35
34
  publishedAt?: string | null;
36
35
  tags?: Array<string | null> | null;
37
36
  metadata?: unknown;
37
+ /**
38
+ * ISO 8601 timestamp from DynamoDB's auto-managed `updatedAt`.
39
+ * Surfaced through the `PublicPost` projection in v0.2 alpha so
40
+ * middleware can compute the `metadata.cache='auto'` cooldown.
41
+ */
42
+ updatedAt?: string | null;
38
43
  }
39
44
  interface PublicPostConnectionShape {
40
45
  items?: Array<PublicPostShape | null> | null;
41
46
  nextToken?: string | null;
42
47
  }
43
48
  interface ListPostsOptions {
44
- siteId?: string;
45
49
  /** ISO 8601 timestamp; SK lower bound (inclusive). */
46
50
  from?: string;
47
51
  /** ISO 8601 timestamp; SK upper bound (inclusive). */
@@ -51,7 +55,6 @@ interface ListPostsOptions {
51
55
  nextToken?: string;
52
56
  }
53
57
  interface ListPostsByTagOptions {
54
- siteId?: string;
55
58
  limit?: number;
56
59
  nextToken?: string;
57
60
  }
@@ -61,9 +64,7 @@ interface ListPostsResult {
61
64
  }
62
65
  interface PostsApi {
63
66
  listPublishedPosts(opts?: ListPostsOptions): Promise<ListPostsResult>;
64
- getPublishedPost(slug: string, opts?: {
65
- siteId?: string;
66
- }): Promise<Post | null>;
67
+ getPublishedPost(slug: string): Promise<Post | null>;
67
68
  listPostsByTag(tag: string, opts?: ListPostsByTagOptions): Promise<ListPostsResult>;
68
69
  }
69
70
 
@@ -87,18 +88,18 @@ interface EffectiveSiteSettings {
87
88
  timezone?: string;
88
89
  }
89
90
  interface SiteSettingsApi {
90
- loadSiteSettings(siteId?: string): Promise<EffectiveSiteSettings>;
91
+ loadSiteSettings(): Promise<EffectiveSiteSettings>;
91
92
  }
92
93
 
93
94
  interface SeoApi {
94
- postMetadata(post: Post, siteId?: string): Promise<Metadata>;
95
- siteMetadata(siteId?: string): Promise<Metadata>;
95
+ postMetadata(post: Post): Promise<Metadata>;
96
+ siteMetadata(): Promise<Metadata>;
96
97
  }
97
98
 
98
99
  interface ThemesRegistry {
99
100
  /** Map of theme name → loaded theme module. */
100
101
  themes: Record<string, ThemeModule>;
101
- /** Name used when no `theme.active` override is stored for the site. */
102
+ /** Name used when no `theme.active` override is stored. */
102
103
  defaultTheme: string;
103
104
  }
104
105
  interface ResolvedTheme {
@@ -106,7 +107,7 @@ interface ResolvedTheme {
106
107
  module: ThemeModule;
107
108
  }
108
109
  interface ThemeActiveApi {
109
- resolveActiveTheme(siteId?: string): Promise<ResolvedTheme>;
110
+ resolveActiveTheme(): Promise<ResolvedTheme>;
110
111
  }
111
112
 
112
113
  type ColorScheme = 'auto' | 'light' | 'dark';
@@ -145,7 +146,7 @@ interface EffectiveThemeConfig {
145
146
  colorScheme: ColorScheme;
146
147
  }
147
148
  interface ThemeConfigApi {
148
- loadThemeConfig(siteId?: string): Promise<EffectiveThemeConfig>;
149
+ loadThemeConfig(): Promise<EffectiveThemeConfig>;
149
150
  }
150
151
  /**
151
152
  * Render `cssVars` as the body of a `:root { ... }` CSS block. Values
@@ -159,32 +160,42 @@ declare function renderBody(post: Post): string;
159
160
  * Convert a tiptap doc to its HTML form. Same renderer the public
160
161
  * site uses. Defensive: tiptap accepts an HTML string as initial
161
162
  * content and parses it on mount, but won't fire onUpdate until the
162
- * user edits so a format-switch chain (e.g. markdown tiptap
163
+ * user edits, so a format-switch chain (e.g. markdown -> tiptap ->
163
164
  * markdown without editing) can still hand us a raw HTML string
164
165
  * here. In that case, return it as-is rather than walking it as a
165
166
  * malformed tiptap node and producing empty output.
166
167
  */
167
168
  declare function tiptapToHtml(doc: unknown): string;
168
- /** Convert markdown to HTML using the built-in minimal renderer. */
169
+ /** Convert markdown to HTML using marked + GFM. */
169
170
  declare function markdownToHtml(md: string): string;
170
171
  /**
171
172
  * Walk a tiptap doc and emit Markdown. Mirrors `renderTiptap` in
172
173
  * shape but produces markdown syntax. Loses anything markdown can't
173
174
  * express (data attributes, image display modes, custom marks).
174
175
  *
176
+ * Notes on info loss:
177
+ * - underline / highlight are not in GFM, so they fall back to the
178
+ * literal `<u>` / `<mark>` HTML tags (preserved as-is across round trips).
179
+ * - paragraph / heading textAlign cannot be expressed in markdown and
180
+ * is therefore lost on conversion.
181
+ *
175
182
  * Same defensive path as tiptapToHtml: a string input means tiptap
176
183
  * hasn't emitted JSON yet (the body is still the HTML we handed it).
177
184
  * Route through htmlToMarkdown so the content survives.
178
185
  */
179
186
  declare function tiptapToMarkdown(doc: unknown): string;
180
187
  /**
181
- * Regex-based HTML Markdown converter. Handles the tag set the
188
+ * Regex-based HTML -> Markdown converter. Handles the tag set the
182
189
  * editor produces (`<p>` `<h1>`-`<h6>` `<strong>` `<em>` `<a>`
183
190
  * `<img>` `<ul>` `<ol>` `<li>` `<code>` `<pre>` `<blockquote>` `<hr>`
184
- * `<br>`). Anything else (tables, sections, divs) keeps its content
185
- * but loses structural meaning.
191
+ * `<br>` `<u>` `<mark>` `<table>` task-list `<ul data-type="taskList">`).
192
+ * Decorative containers like `<div style="text-align:...">` are dropped.
193
+ *
194
+ * Tables are reduced to GFM pipe syntax via convertHtmlTable. Complex
195
+ * nested content inside cells (lists, other tables) is flattened to
196
+ * plain text.
186
197
  *
187
- * Not a full library there are known limits like nested formatting
198
+ * Not a full library, there are known limits like nested formatting
188
199
  * inside list items potentially merging. Acceptable for a v0.x
189
200
  * format-switch convenience; complex HTML round-trips shouldn't be
190
201
  * relied on.
@@ -198,15 +209,13 @@ interface CreateAmplessOpts {
198
209
  }
199
210
  interface Ampless {
200
211
  listPublishedPosts(opts?: ListPostsOptions): Promise<ListPostsResult>;
201
- getPublishedPost(slug: string, opts?: {
202
- siteId?: string;
203
- }): Promise<Post | null>;
212
+ getPublishedPost(slug: string): Promise<Post | null>;
204
213
  listPostsByTag(tag: string, opts?: ListPostsByTagOptions): Promise<ListPostsResult>;
205
- loadSiteSettings(siteId?: string): Promise<EffectiveSiteSettings>;
206
- resolveActiveTheme(siteId?: string): Promise<ResolvedTheme>;
207
- loadThemeConfig(siteId?: string): Promise<EffectiveThemeConfig>;
208
- postMetadata(post: Post, siteId?: string): Promise<Metadata>;
209
- siteMetadata(siteId?: string): Promise<Metadata>;
214
+ loadSiteSettings(): Promise<EffectiveSiteSettings>;
215
+ resolveActiveTheme(): Promise<ResolvedTheme>;
216
+ loadThemeConfig(): Promise<EffectiveThemeConfig>;
217
+ postMetadata(post: Post): Promise<Metadata>;
218
+ siteMetadata(): Promise<Metadata>;
210
219
  renderBody(post: Post): string;
211
220
  renderThemeCss(cssVars: Record<string, string>): string;
212
221
  publicAssetUrl(key: string): string;